Asynchronous mutual exclusion pipes
AMP - Async Mutex Pipes
Den mest centrala delen i systemet vad gäller JavaScript-delen. Lägger grunden för sättet att strukturera resten av systemet. Avsikten är att respektera web-programs asynkrona natur (flera händelser kan ske samtidigt). Att ha ett linjärt tänkande och samtidigt ge sig an ett större JavaScript-projekt är dömt att leda till svårigheter.
Min lösning på detta är under utveckling.
Jag tänker att det linjära tänkandet (procedurisk programmering) gestaltas inte enbart i hur koden struktureras men också i hur data skickas mellan komponenter. Ett vanligt problem är att en komponent behöver ett ID-nunmer av en annan komponent. Det ID-numret måste då skickas via hela kedjan av funktionsanrop vilket leder till hårt kopplad kod som blir in flexibel.
Event-centrerad programmering är ett alternativ och fungerar bra i JavaScript. Men att göra ett event baserat system och ändå skicka argument via anropa-kedjor är inte tillräckligt bra. Detta problem kan ses när vi har en "controller"-klass som ska rendera ui-element. Du som användare av en hypotetisk sajt har en lista framför sig med 40 poster och trycker på knappen "visa post 23" Den posten måste då skicka med ID 23 upp till controller som sedan väljer post-vyn och skickar med ID in i det. Även om vår "Controller" klass lyssnar på ett event och agerar där efter, så känns det "proceduriskt" att behöva skicla ID från list-vyn upp till Controller-klassen och sedan ner till post-vyn. Det skickas vanligtvis tillsammans med event-händelsen. De fördelar vi kunde vinna via att använda en event-struktur förhindras vi när datan passeras på detta sätt. Koden blir I flexibel och hårt kopplad. Om Controller klassen däremot inte behöver handskas med ID-numret överhuvud taget, då är koden mer löst kopplad och reflekterar mer av en lösning som svarar mot asynkrona system. Jag har hittat en lösning på det, vilket ger ett helt annat sätt att se på koden. Lösningen är som följer: [inte färdigskrivet]
Egenskaper
-
Create any number of pipes, at any depth of nesting (pipe.a.b.c...).
-
An internal queue of data in the pipe.
-
Keeping track of each listeners index in the queue. A listener returns later and continues from where it left off regardless of how many new items have been added to the queue since last time it checked.
-
The internal queue is cleared of data that all active listeners have moved past.
-
A listener takes one data-element at a time from the queue.
-
Provides a pipe for logs for debugging (pipe "_", that is: pipe.[underscore]). Combine it with another moduel listening on that pipe and rendering a realtime map of all pipes and their i/o.
-
When starting to listen the prior data in the queue is not returned. The listener will recieve the next data comming in. (Easy to extend with function to return current queue if needed but shouldn't be needed).
-
Mutex on both set and get functionality. Multiple calls to
get
orset
are executed sequentially preventing two processes from writing to the same pipe at the same time. Eitherget
orset
can be executed at any given time. Prevents conflict between read and write operations on the pipe. -
Mutexes for set/get are pipe-specific. Each pipe has its own mutex.
Requirements
- clearUniqueId - clearing a listener (to remove from internal position and waiting list, as to not prevent clearing old data in queue) pipe.a.b.c.clear
- name the listeners
Why mutex on both get/set?
Why not just on set? Because no mutex on also set can give data-skipping and out-of-sync state.
Without a mutex in the get()
method, the following problem can occur:
-
Initial Check Misses New Data: The process (let's call it Process A) checks for new data but doesn't find any. At this point, it thinks there's no data available.
-
Another Process Adds Data: While Process A is still executing, another process (Process B) adds new data and resolves all existing promises for any waiting processes.
-
Process A Creates a New Promise: After Process B adds data and resolves the promises, Process A continues its execution. It reaches the point where it decides to wait for new data by creating a promise. However, since Process B has already resolved all promises, this new promise won't be resolved until more data is added in the future.
-
Missed and Skipped Data: The promise created by Process A should have been resolved immediately with the data added by Process B, but instead, it waits for the next data update. When new data eventually arrives, the promise resolves with the most recent data. As a result, Process A skips the data that was added by Process B and only processes the most recent data. Additionally, Process A's index is now out of sync—it still points to the first piece of data, so the next time it checks, it might process the wrong data.
-
Out-of-Sync State: This causes Process A to skip the first piece of data and then potentially process the second piece twice, leading to a situation where data is skipped or processed incorrectly.
Race conditions:
Mutex is put on the set() functionality. Each pipe having its own mutex.
Simultaneous Writes: If two processes write to the queue at the exact same time, the operations could interfere with each other, leading to incorrect indices or overwriting data.
Simultaneous Reads and Writes: A process could be reading from the queue while another is writing to it, leading to inconsistent data being returned or incorrect listener resolution.
Läsa på
Promise/resolve är en central del för asynkrona operationer i JavaScript, varav jag avser förstå det i detalj. Jag behöver läsa på mer om detta.
"Promise chaining" är en teknik som används för att skapa en sekvens av asynkrona operationer. Detta är användbart när en operation måste utföras efter en annan.
(Detta är modulen som garanterar att enbart en process utför ett särskilt omfång av kod åt gången.)
Koden nedan fungerar, men den utmanar ens förståelse kring hur asynkron kod i JS fungerar.
class Mutex {
constructor() {
this._lock = Promise.resolve();
}
lock() {
let unlockNext;
const willLock = new Promise(resolve => unlockNext = resolve);
const willUnlock = this._lock.then(() => unlockNext);
this._lock = willUnlock;
return willUnlock;
}
}
// Example
async set(data) {
await this._mutex.lock(); // Lock before writing to the queue
try {
await this._set(data);
} finally {
this._mutex.lock(); // Release the lock
}
}
Resurser:
- https://javascript.info/promise-chaining
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Del av hur moduler tillkännager och bevakar uppdatering av data. Mest basala modul för hur alla asynkrona komponenter kommunicerar (som när användare trycker på knapp och annan del av sidan ska uppdateras).
Notiser
set/get mutex
En Mutex på set() som tvingar alla som anropar get att vänta, men om ingen set-mutex är satt så behöver de inte vänta utan kan tillgå promise och vänta där.
Separation of concerns
Ett pipe-objelt kan skapas för enskild komponent och dess komplexa innehåll med interkommunikation. Lågnivå pipe systems blandas inte med högre nivå.
Fel
Problem
Nuvarande lösning, fortfarande problem. En process låser som förväntat. De andra väntar på att kunna gå till promise-delen eftersom de inte fanns ny data. När låset öppnas och den släpps in har ny data redan kommit till den första som låste och index ökat. De andra processerna förväntar samma index som tidigare.
Nästa process på tur måste kontrollera index innan promise-delen gås in i och undvika promise om index ändrats, då bara returnera den nya datan. Eftersom nästa på tur går in i ett Mutex lås, så är det garanterat att index inte ändras den osannolika millisekunden mellan kontroll av index och tillgång av promise.
Om en process kör get() så låses get/set Mutex. Hur kan då set sätta data om den väntar på att get() ska vara klar. get() väntar ju på att set() ska sätta data.
Fixat
Utlåning
Det är ett fel i get()
. Den låser sig som förväntat för ett anrop som väntar på ny data. Blockerar de som ska läsa tidigare data. De måste vänta på att ny data kommer och att låset därmed öppnas av den som väntar på det.
Låt processer med lägre kö-id än det högsta gå igenom. De avser inte att vänta på data eftersom deras data redan finns. Ska inte vara några race conditions eller out-of-sync här eftersom de bara läser gammal data och är opåverkade av vad som läggs till i slutet av kön. Även att kö-element bara raderas som har lägre index än index på den process som har
Js delete, doesn't affect array/map length? Must not.